﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using log4net;
using PortableVersion.Core.General;
using PortableVersion.Core.SourceControl;

namespace PortableVersion.Core.FileSystem.Services
{
    /// <summary>
    /// Class that applies diffs and resolves the conflicts
    /// </summary>
    public class DiffApplicationService : IDiffApplicationService<FileItem>, IConflictResolutionService<FileItem>
    {
        private readonly IFsoService _fso;
        private readonly ISourceControlService _sourceControlService;
        private readonly ILog _log;
        private readonly List<Diff<FileItem>> _diffs = new List<Diff<FileItem>>();
        private readonly List<Diff<FileItem>> _conflicts = new List<Diff<FileItem>>();
        private ConflictState _conflictState = new ConflictState();
        private string _targetRoot;
        private string _sourceRoot;

        public void ClearConflictAction()
        {
            _conflictState.Items.Clear();
        }

        public void DeleteConflictAction(ConflictItem conflict)
        {
            _conflictState.Items.Remove(_conflictState.Items.Where(c => c.Id.Equals(conflict.Id)).First());
        }

        public void AddConflictAction(Diff<FileItem> conflict, ConflictAction conflictAction)
        {
            var ci = new ConflictItem
                         {
                             ActionTaken = conflictAction,
                             MetaKey = conflict.Meta.Key,
                             MonitoredTime = DateTime.UtcNow,
                             TargetPath = conflict.Target.Path,
                             SourcePath = conflict.Source.Path
                         };
            _conflictState.Items.Add(ci);
        }

        public ConflictState ConflictState {get{return _conflictState;}}
        public void QueueDiffApplication(Diff<FileItem> safeDiff) 
        {
            _diffs.Add(safeDiff);
        }

        /// <summary>
        /// Apply safe changes
        /// </summary>
        public IEnumerable<DiffApplicationResult> ApplyAllDiffs()
        {
            if (String.IsNullOrEmpty(_sourceRoot) || String.IsNullOrEmpty(_targetRoot))
            {
                throw new InvalidOperationException("Source and target root must be set before applying diffs.");
            }
            return _diffs.Where(d => d.DiffType == DiffType.Safe).Select(ApplyDiff);
        }

        private DiffApplicationResult ApplyDiff(Diff<FileItem> diff)
        {
            try
            {
                _sourceControlService.SetRoot(_targetRoot);
                if (diff.SourceEvent == DiffEvent.Modified)
                {
                    _sourceControlService.CheckOut(diff.Target.Path);
                    _fso.CopyFile(diff.Source, diff.Target,
                                  FileCopyOptions.Overwrite | FileCopyOptions.KeepTargetCreatedTime);
                }
                if (diff.SourceEvent == DiffEvent.Added)
                {
                    var addedItem = FileSystemHelper.ReplaceCommonPath(_sourceRoot, _targetRoot, diff.Source.Path);
                    if (diff.IsFolder)
                    {
                        _sourceControlService.AddFolder(addedItem);
                    }
                    else
                    {
                        _fso.CopyFile(diff.Source.Path, addedItem,
                                      FileCopyOptions.Overwrite | FileCopyOptions.KeepTargetCreatedTime);
                        _sourceControlService.Add(addedItem);
                    }
                }
                if (diff.SourceEvent == DiffEvent.Renamed)
                {
                    var newName = FileSystemHelper.ReplaceCommonPath(_sourceRoot, _targetRoot, diff.Source.Path);
                    if (diff.IsFolder)
                    {
                        _sourceControlService.RenameFolder(diff.Target.Path, newName);
                    }
                    else
                    {
                        _sourceControlService.Rename(diff.Target.Path, newName);
                        _sourceControlService.CheckOut(newName);
                        //Source control should have already renamed the file, so just have to overwrite the content.
                        _fso.CopyFile(diff.Source.Path, newName,
                                      FileCopyOptions.Overwrite | FileCopyOptions.KeepTargetCreatedTime);
                    }
                }
                if (diff.SourceEvent == DiffEvent.Deleted)
                {
                    if (diff.Target.IsFolder)
                    {
                        _sourceControlService.DeleteFolder(diff.Target.Path);
                    }
                    else
                        _sourceControlService.Delete(diff.Target.Path);
                }
            }
            catch(SourceControlException exception)
            {
                return new DiffApplicationResult(diff, exception);
            }
            return  new DiffApplicationResult(diff, null);
        }

        public void QueueConflict(Diff<FileItem> conflict)
        {
            _conflicts.Add(conflict);
        }

        public void LoadState(string path)
        {
            _log.InfoFormat(@"Loding conflict state from '{0}'", path);
            if (!File.Exists(path))
            {
                _log.Info(@"Conflict state file does not exist.");
                _conflictState = new ConflictState();
                return;
            }
            try
            {
                _conflictState = SerializationHelper.Load<ConflictState>(path);
                _log.Info(@"Conflict state loaded.");
            }
            catch 
            {
                _log.ErrorFormat(@"Could not load the conflict state file '{0}'", path);
                throw;
            }
        }

        /// <summary>
        /// Applies (adds/updates) actions to the ConflictState. 
        /// </summary>
        /// <param name="actions"></param>
        public void ApplyActions(List<ConflictItem> actions)
        {
            foreach (var conflictItem in actions)
            {
                var item = conflictItem;
                var existing = _conflictState.Items.FirstOrDefault(cs => cs.MetaKey == item.MetaKey);
                if (existing != null)
                    _conflictState.Items.Remove(existing);
                _conflictState.Items.Add(item);
            }
        }

        public void SaveState(string path)
        {
            SerializationHelper.Save(_conflictState, path);
        }

        public IEnumerable<Diff<FileItem>> GetUnhandledConflicts()
        {
            ValidateConflictState();
            return _conflicts.Where(c => !_conflictState.Items.Any(si => c.Meta.Key == si.MetaKey));
        }

        /// <summary>
        /// Make sure that conflicts are still valid. Conflict is valid if the target file
        /// is not modified after the monitor period.
        /// </summary>
        private void ValidateConflictState()
        {
            _log.Info(@"Validating conflicts.");
            var invalids = new List<ConflictItem>();
            foreach (var conf in from conf in _conflictState.Items
                                 let modiTime = (conf.TargetPath != null) ? File.GetLastWriteTimeUtc(conf.TargetPath) : DateTime.MinValue
                                 where modiTime > conf.MonitoredTime || (conf.ActionTaken == ConflictAction.Merge && !File.Exists(conf.TargetPath + "." + conf.Id))
                                 || WrongActionSelected(conf)
                                 select conf)
            {
                invalids.Add(conf);
                _log.InfoFormat(@"Conflict action for {0} => {1} was removed.", conf.TargetPath, conf.ActionTaken);
            }
            invalids.ForEach(i => _conflictState.Items.Remove(i));
        }

        /// <summary>
        /// Wrong action is:
        ///     If conflictType is LocationConflict, only Ignore can be selected.
        ///     If conflictType is ContentConflict, any action can be selected
        /// </summary>
        /// <param name="conf"></param>
        /// <returns></returns>
        private bool WrongActionSelected(ConflictItem conf)
        {
            var diff = _conflicts.FirstOrDefault(c => c.Meta.Key == conf.MetaKey);
            if (diff == null) //The conflict does not exists anymore
                return false;
            if (diff.DiffType == DiffType.LocationConflict && (
                !(conf.ActionTaken == ConflictAction.Ignore || conf.ActionTaken == ConflictAction.Manual)))
                return true;
            return false;
        }

        public IEnumerable<DiffApplicationResult> ResolveAllConflicts()
        {
            if (String.IsNullOrEmpty(_sourceRoot) || String.IsNullOrEmpty(_targetRoot))
            {
                throw new InvalidOperationException("Source and target root must be set before applying diffs.");
            }
            _sourceControlService.SetRoot(_targetRoot);
            ValidateConflictState();
            if (_conflicts.Any(c => !_conflictState.Items.Any(ci => ci.MetaKey == c.Meta.Key)))
            {
                throw new InvalidOperationException(String.Format(@"Can not check in conflicts, because there are still unresolved conflicts."));
            }
            _log.Info(@"All conflicts have actions. Started resolving conflicts.");
            return _conflicts.Select(ResolveConflict);
        }

        private DiffApplicationResult ResolveConflict(Diff<FileItem> conflict)
        {
            try
            {
                var conf = conflict;
                var action = _conflictState.Items.First(ci => ci.MetaKey == conf.Meta.Key);
                switch (action.ActionTaken)
                {
                    case ConflictAction.Ignore:
                        _log.InfoFormat(@"Ignoring the conflict for {0} (Keeping master version)",
                                        conf.Source.NullSafe(t => t.Path) ?? conf.Meta.Path);
                        break;
                    case ConflictAction.Manual:
                        _log.InfoFormat(
                            @"Conflict between '{0}' and '{1}' where the slave is {2} and master is {3} should be resolved manually. We don't know what to do with it.",
                            conf.Source.NullSafe(s => s.Path) ?? conf.Meta.Path
                            , conf.Target.NullSafe(t => t.Path) ?? conf.Meta.Path,
                            conf.SourceEvent, conf.TargetEvent);
                        break;
                    case ConflictAction.Overwrite:
                        OverwriteConflict(action);
                        break;
                    case ConflictAction.Merge:
                        MergeConflict(action);
                        break;
                }
            }
            catch(SourceControlException exception)
            {
                return new DiffApplicationResult(conflict, exception);
            }
            return new DiffApplicationResult(conflict, null);
        }

        private void MergeConflict(ConflictItem conf)
        {
            var mergeFile = conf.TargetPath + "." + conf.Id;
            _log.InfoFormat(@"Applying the merged conflict from '{0}' to {1}.", mergeFile, conf.TargetPath);
            _sourceControlService.CheckOut(conf.TargetPath);
            _fso.CopyFile(mergeFile, conf.TargetPath, FileCopyOptions.Overwrite | FileCopyOptions.KeepTargetCreatedTime);
            _fso.DeleteFile(mergeFile);
        }

        private void OverwriteConflict(ConflictItem conf)
        {
            _log.InfoFormat(@"Overwriting '{0}' on '{1}'.", conf.SourcePath, conf.TargetPath);
            _sourceControlService.CheckOut(conf.TargetPath);
            _fso.CopyFile(conf.SourcePath, conf.TargetPath, FileCopyOptions.Overwrite | FileCopyOptions.KeepTargetCreatedTime );
        }

        public void SetRootPaths(string source, string target)
        {
            _sourceRoot = source;
            _targetRoot = target;
        }

        public DiffApplicationService(IFsoService fso, ISourceControlService sourceControlService, ILog log)
        {
            _fso = fso;
            _sourceControlService = sourceControlService;
            _log = log;
        }
    }
}
